/**
 * @author cesarpachon
 */
#include <cstring>
#include <cstdint>
#include "audio/linux/AudioEngine-linux.h"

#include "base/CCDirector.h"
#include "base/CCScheduler.h"
#include "platform/CCFileUtils.h"

using namespace cocos2d;
using namespace cocos2d::experimental;

AudioEngineImpl * g_AudioEngineImpl = nullptr;

void ERRCHECKWITHEXIT(FMOD_RESULT result)
{
    if (result != FMOD_OK) {
        printf("FMOD error! (%d) %s\n", result, FMOD_ErrorString(result));
    }
}

bool ERRCHECK(FMOD_RESULT result)
{
    if (result != FMOD_OK) {
        printf("FMOD error! (%d) %s\n", result, FMOD_ErrorString(result));
        return true;
    }
    return false;
}

FMOD_RESULT F_CALLBACK channelCallback(FMOD_CHANNELCONTROL *channelcontrol,
                                       FMOD_CHANNELCONTROL_TYPE controltype,
                                       FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype,
                                       void *commandData1, void *commandData2)
{
    if (controltype == FMOD_CHANNELCONTROL_CHANNEL && callbacktype == FMOD_CHANNELCONTROL_CALLBACK_END) {
        g_AudioEngineImpl->onSoundFinished((FMOD::Channel *)channelcontrol);
    }
    return FMOD_OK;
}

AudioEngineImpl::AudioEngineImpl()
{
}

AudioEngineImpl::~AudioEngineImpl()
{
    FMOD_RESULT result;
    result = pSystem->close();
    ERRCHECKWITHEXIT(result);
    result = pSystem->release();
    ERRCHECKWITHEXIT(result);
}

bool AudioEngineImpl::init()
{
    FMOD_RESULT result;
    /*
    Create a System object and initialize.
    */
    result = FMOD::System_Create(&pSystem);
    ERRCHECKWITHEXIT(result);

    result = pSystem->setOutput(FMOD_OUTPUTTYPE_AUTODETECT);
    ERRCHECKWITHEXIT(result);

    result = pSystem->init(32, FMOD_INIT_NORMAL, 0);
    ERRCHECKWITHEXIT(result);

    mapChannelInfo.clear();
    mapSound.clear();

    auto scheduler = cocos2d::Director::getInstance()->getScheduler();
    scheduler->schedule(schedule_selector(AudioEngineImpl::update), this, 0.05f, false);

    g_AudioEngineImpl = this;

    return true;
}

int AudioEngineImpl::play2d(const std::string &fileFullPath, bool loop, float volume)
{
    int id = preload(fileFullPath, nullptr);
    if (id >= 0) {
        mapChannelInfo[id].loop=loop;
        mapChannelInfo[id].channel->setPaused(true);
        mapChannelInfo[id].volume = volume;
        AudioEngine::_audioIDInfoMap[id].state = AudioEngine::AudioState::PAUSED;
        resume(id);
    }
    return id;
}

void AudioEngineImpl::setVolume(int audioID, float volume)
{
    try {
        mapChannelInfo[audioID].channel->setVolume(volume);
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::setVolume: invalid audioID: %d\n", audioID);
    }
}

void AudioEngineImpl::setLoop(int audioID, bool loop)
{
    try {
        mapChannelInfo[audioID].channel->setLoopCount(loop ? -1 : 0);
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::setLoop: invalid audioID: %d\n", audioID);
    }
}

bool AudioEngineImpl::pause(int audioID)
{
    try {
        mapChannelInfo[audioID].channel->setPaused(true);
        AudioEngine::_audioIDInfoMap[audioID].state = AudioEngine::AudioState::PAUSED;
        return true;
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::pause: invalid audioID: %d\n", audioID);
        return false;
    }
}

bool AudioEngineImpl::resume(int audioID)
{
    try {
        if (!mapChannelInfo[audioID].channel) {
            FMOD::Channel *channel = nullptr;
            FMOD::ChannelGroup *channelgroup = nullptr;
            //starts the sound in pause mode, use the channel to unpause
            FMOD_RESULT result = pSystem->playSound(mapChannelInfo[audioID].sound, channelgroup, true, &channel);
            if (ERRCHECK(result)) {
                return false;
            }
            channel->setMode(mapChannelInfo[audioID].loop ? FMOD_LOOP_NORMAL : FMOD_LOOP_OFF);
            channel->setLoopCount(mapChannelInfo[audioID].loop ? -1 : 0);
            channel->setVolume(mapChannelInfo[audioID].volume);
            channel->setUserData(reinterpret_cast<void *>(static_cast<std::intptr_t>(mapChannelInfo[audioID].id)));
            mapChannelInfo[audioID].channel = channel;
        }

        mapChannelInfo[audioID].channel->setPaused(false);
        AudioEngine::_audioIDInfoMap[audioID].state = AudioEngine::AudioState::PLAYING;

        return true;
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::resume: invalid audioID: %d\n", audioID);
        return false;
    }
}

bool AudioEngineImpl::stop(int audioID)
{
    try {
        mapChannelInfo[audioID].channel->stop();
        mapChannelInfo[audioID].channel = nullptr;
        return true;
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::stop: invalid audioID: %d\n", audioID);
        return false;
    }
}

void AudioEngineImpl::stopAll()
{
    for (auto& it : mapChannelInfo) {
        ChannelInfo & audioRef = it.second;
        audioRef.channel->stop();
        audioRef.channel = nullptr;
    }
}

float AudioEngineImpl::getDuration(int audioID)
{
    try {
        FMOD::Sound * sound = mapChannelInfo[audioID].sound;
        unsigned int length;
        FMOD_RESULT result = sound->getLength(&length, FMOD_TIMEUNIT_MS);
        ERRCHECK(result);
        float duration = (float)length / 1000.0f;
        return duration;
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::getDuration: invalid audioID: %d\n", audioID);
        return AudioEngine::TIME_UNKNOWN;
    }
}

float AudioEngineImpl::getCurrentTime(int audioID)
{
    try {
        unsigned int position;
        FMOD_RESULT result = mapChannelInfo[audioID].channel->getPosition(&position, FMOD_TIMEUNIT_MS);
        ERRCHECK(result);
        float currenttime = position /1000.0f;
        return currenttime;
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::getCurrentTime: invalid audioID: %d\n", audioID);
        return AudioEngine::TIME_UNKNOWN;
    }
}

bool AudioEngineImpl::setCurrentTime(int audioID, float time)
{
    bool ret = false;
    try {
        unsigned int position = (unsigned int)(time * 1000.0f);
        FMOD_RESULT result = mapChannelInfo[audioID].channel->setPosition(position, FMOD_TIMEUNIT_MS);
        ret = !ERRCHECK(result);
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::setCurrentTime: invalid audioID: %d\n", audioID);
    }
    return ret;
}

void AudioEngineImpl::setFinishCallback(int audioID, const std::function<void (int, const std::string &)> &callback)
{
    try {
        FMOD::Channel * channel = mapChannelInfo[audioID].channel;
        mapChannelInfo[audioID].callback = callback;
        FMOD_RESULT result = channel->setCallback(channelCallback);
        ERRCHECK(result);
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::setFinishCallback: invalid audioID: %d\n", audioID);
    }
}

void AudioEngineImpl::onSoundFinished(FMOD::Channel * channel)
{
    int id = 0;
    try {
        void * data;
        channel->getUserData(&data);
        id = static_cast<int>(reinterpret_cast<std::intptr_t>(data));
        if (mapChannelInfo[id].callback) {
            mapChannelInfo[id].callback(id, mapChannelInfo[id].path);
        }
        mapChannelInfo[id].channel = nullptr;
    }
    catch (const std::out_of_range& oor) {
        printf("AudioEngineImpl::onSoundFinished: invalid audioID: %d\n", id);
    }
}

void AudioEngineImpl::uncache(const std::string& path)
{
    std::string fullPath = FileUtils::getInstance()->fullPathForFilename(path);
    std::map<std::string, FMOD::Sound *>::const_iterator it = mapSound.find(fullPath);
    if (it!=mapSound.end()) {
        FMOD::Sound * sound = it->second;
        if (sound) {
            sound->release();
        }
        mapSound.erase(it);
    }
    if (mapId.find(path) != mapId.end())
        mapId.erase(path);
}

void AudioEngineImpl::uncacheAll()
{
    for (const auto& it : mapSound) {
        auto sound = it.second;
        if (sound) {
            sound->release();
        }
    }
    mapSound.clear();
    mapId.clear();
}

int AudioEngineImpl::preload(const std::string& filePath, std::function<void(bool isSuccess)> callback)
{
    FMOD::Sound * sound = findSound(filePath);
    if (!sound) {
        std::string fullPath = FileUtils::getInstance()->fullPathForFilename(filePath);
        FMOD_RESULT result = pSystem->createSound(fullPath.c_str(), FMOD_LOOP_OFF, 0, &sound);
        if (ERRCHECK(result)) {
            printf("sound effect in %s could not be preload\n", filePath.c_str());
            if (callback) {
                callback(false);
            }
            return -1;
        }
        mapSound[fullPath] = sound;
    }

    int id = static_cast<int>(mapChannelInfo.size()) + 1;
    if (mapId.find(filePath) == mapId.end())
        mapId.insert({filePath, id});
    else
        id = mapId.at(filePath);

    auto& chanelInfo = mapChannelInfo[id];
    chanelInfo.sound = sound;
    chanelInfo.id = id;
    chanelInfo.channel = nullptr;
    chanelInfo.callback = nullptr;
    chanelInfo.path = filePath;
    //we are going to use UserData to store pointer to Channel when playing
    chanelInfo.sound->setUserData(reinterpret_cast<void *>(static_cast<std::intptr_t>(id)));

    if (callback) {
        callback(true);
    }
    return id;
}

void AudioEngineImpl::update(float dt)
{
    pSystem->update();
}

FMOD::Sound * AudioEngineImpl::findSound(const std::string &path)
{
    std::string fullPath = FileUtils::getInstance()->fullPathForFilename(path);
    std::map<std::string, FMOD::Sound *>::const_iterator it = mapSound.find(fullPath);
    return (it != mapSound.end()) ? (it->second) : nullptr;
}

FMOD::Channel * AudioEngineImpl::getChannel(FMOD::Sound *sound)
{
    void * data;
    sound->getUserData(&data);
    int id = static_cast<int>(reinterpret_cast<std::intptr_t>(data));
    return mapChannelInfo[id].channel;
}
